package jmine.tec.persist.jms;


import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.Enumeration;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.MessageDrivenBean;
import javax.ejb.Remove;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.naming.NamingException;

import jmine.tec.persist.jms.domain.JMSStoredMessage;
import jmine.tec.utils.io.IOUtils;

import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.access.BeanFactoryLocator;
import org.springframework.beans.factory.access.BeanFactoryReference;
import org.springframework.context.access.ContextSingletonBeanFactoryLocator;
import org.springframework.ejb.support.AbstractMessageDrivenBean;
import org.springframework.jndi.JndiTemplate;

import bancosys.tec.persist.audit.AuditManager;
import bancosys.tec.persist.controller.TransactionalController;
import bancosys.tec.rtm.ExceptionContext;
import bancosys.tec.rtm.exception.ExceptionPersister;

/**
 * MDB que delega para um MessageListener registrado no spring. O nome do messageListener, bem como a referencia à beanFactory devem ser
 * setadas no ejb-jar.xml. Alternativamente, este MDB pode ser configurado pelo spring e seus metodos de callback analogos aos metodos de
 * callback de um EJB serao invocados - porem o MDB nao tera acesso ao MessageDrivenContext.
 * 
 * @author takeshi
 */
public class PersistentMessageMDB extends AbstractMessageDrivenBean implements BeanFactoryAware, InitializingBean, DisposableBean,
        MessageListener {
    public static final String MESSAGE_LISTENER_BEAN_NAME_KEY = "java:comp/env/jms/MessageListener";

    private MessageListener messageListener;

    private String messageListenerBeanName;

    private TransactionalController transactionalController;

    private String transactionalControllerBeanName;

    /**
     * C'tor
     * 
     * @param beanFactory {@link BeanFactory}
     */
    public PersistentMessageMDB(BeanFactory beanFactory) {
        this(new FixedBeanFactoryLocator(beanFactory));
    }

    /**
     * C'tor que vai usar o {@link ContextSingletonBeanFactoryLocator} default
     */
    public PersistentMessageMDB() {
        this(ContextSingletonBeanFactoryLocator.getInstance());
    }

    /**
     * C'tor que vai usar o {@link ContextSingletonBeanFactoryLocator} com o selector passado no construtor
     * 
     * @param beanFactorySelector {@link String}
     */
    public PersistentMessageMDB(String beanFactorySelector) {
        this(ContextSingletonBeanFactoryLocator.getInstance(beanFactorySelector));
    }

    /**
     * C'tor que vai usar o {@link BeanFactoryLocator} passado
     * 
     * @param beanFactoryLocator {@link BeanFactoryLocator}
     */
    public PersistentMessageMDB(BeanFactoryLocator beanFactoryLocator) {
        setBeanFactoryLocator(beanFactoryLocator);
        setBeanFactoryLocatorKey(lookupString(BEAN_FACTORY_PATH_ENVIRONMENT_KEY));
    }

    /**
     * @param path String
     * @return String
     */
    private static String lookupString(String path) {
        try {
            return (String) new JndiTemplate().lookup(path, String.class);
        } catch (NamingException e) {
            return path;
        }
    }

    /**
     * Callback de {@link MessageListener}. Este metodo vai 'comer' eventuais excecoes e salva-las como {@link JMSStoredMessage} e, se
     * conseguir
     * 
     * @param msg {@link Message}
     */
    public void onMessage(Message msg) {
        final ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
        final ClassLoader beanFactoryClassLoader = getClass().getClassLoader();
        if (currentClassLoader != beanFactoryClassLoader) {
            Thread.currentThread().setContextClassLoader(beanFactoryClassLoader);
        }
        try {
            ExceptionContext.initialize();
            AuditManager.setAuditSource("<MDB@" + msg.getJMSDestination() + ">");
            MessageListener delegate = getMessageListener();
            if (this.messageListenerBeanName != null) {
                ExceptionContext.getCurrentContext().put("messageListenerBeanName", this.messageListenerBeanName);
            } else {
                ExceptionContext.getCurrentContext().put("messageListenerDelegate", String.valueOf(delegate));
            }
            if (delegate instanceof MessageDrivenBean) {
                ((MessageDrivenBean) delegate).setMessageDrivenContext(getMessageDrivenContext());
            }
            delegate.onMessage(msg);
        } catch (Throwable e) {
            onError(msg, e);
        } finally {
            if (currentClassLoader != beanFactoryClassLoader) {
                Thread.currentThread().setContextClassLoader(currentClassLoader);
            }
            ExceptionContext.cleanup();
            AuditManager.setAuditSource(null);

        }
    }

    /**
     * Chamado quando ocorre um erro ao processar uma mensagem
     * 
     * @param msg {@link Message}
     * @param e Throwable
     */
    protected void onError(Message msg, Throwable e) {
        try {
            saveMessage(msg, e);
            recordRTM(e);
        } finally {
            LogFactory.getLog(PersistentMessageMDB.class).error(formatMessageType(msg), e);
        }
    }

    /**
     * Formata a mensagem, colocando algumas informacoes como tipo e propriedades
     * 
     * @param msg {@link Message}
     * @return String
     */
    private static String formatMessageType(Message msg) {
        StringBuilder sb = new StringBuilder("MessageType: ");
        try {
            sb.append(msg.getJMSType());
            sb.append("\nProperties: \n");
            Enumeration<?> propertyNames = msg.getPropertyNames();
            while (propertyNames.hasMoreElements()) {
                String propName = (String) propertyNames.nextElement();
                Object obj = msg.getObjectProperty(propName);
                sb.append("\n").append(propName).append(": ").append(String.valueOf(obj));
            }
            sb.append("\nredelivered?: ").append(msg.getJMSRedelivered());
            sb.append("\nreplyTo: ").append(msg.getJMSReplyTo());
            sb.append("\ndestination: ").append(msg.getJMSDestination());
            sb.append("\nmessageID: ").append(msg.getJMSMessageID());
        } catch (JMSException e) {
            sb.append(e.getMessage());
            sb.append(Arrays.toString(e.getStackTrace()));
        }
        return sb.toString();
    }

    /**
     * Procura na beanFactory instancias de {@link ExceptionPersister} para salvar a excecao
     * 
     * @param e Throwable
     */
    protected void recordRTM(Throwable e) {
        BeanFactory bf = getBeanFactory();
        if (bf instanceof ListableBeanFactory) {
            String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(((ListableBeanFactory) bf), ExceptionPersister.class);
            for (String name : beanNames) {
                ExceptionPersister pers = (ExceptionPersister) bf.getBean(name);
                try {
                    pers.monitor(e);
                } catch (Throwable e1) {
                    // ignoring exceptions
                    e1.printStackTrace();
                }
            }
            if (beanNames.length == 0) {
                e.printStackTrace();
            }
        }
    }

    /**
     * Persiste a mensagem junto com o erro ocorrido
     * 
     * @param msg {@link Message}
     * @param e {@link Throwable}
     */
    protected void saveMessage(Message msg, Throwable e) {
        final TransactionalController txController = getTransactionalController();
        if (txController != null) {
            txController.startTransaction();
        }
        boolean commit = false;
        try {
            if (getBeanFactory().containsBean("jmsStoreController")) {
                JMSStoreController controller = (JMSStoreController) getBeanFactory().getBean("jmsStoreController");
                JMSStoredMessage bean = controller.getDAOFactory().getGenericDAO(JMSStoredMessage.class).createBean();
                bean.setListenerBeanName(getMessageListenerBeanName());
                bean.setMessage(IOUtils.serializeObject(msg));
                if (e != null) {
                    final StringWriter result = new StringWriter();
                    final PrintWriter printWriter = new PrintWriter(result);
                    e.printStackTrace(printWriter);
                    bean.setErrorMessage(result.toString());
                }
                bean.getPersister().save();
                commit = true;
            }
        } finally {
            if (txController != null) {
                if (commit) {
                    txController.commit();
                } else {
                    txController.rollback();
                }
            }
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory)
     */
    public void setBeanFactory(BeanFactory beanFactory) {
        this.setBeanFactoryLocator(new FixedBeanFactoryLocator(beanFactory));
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.springframework.ejb.support.AbstractMessageDrivenBean#onEjbCreate()
     */
    @Override
    protected void onEjbCreate() {
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
     */
    public void afterPropertiesSet() throws Exception {
        this.ejbCreate();
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.springframework.beans.factory.DisposableBean#destroy()
     */
    public void destroy() throws Exception {
        this.ejbRemove();
    }

    /**
     * Implementacao simples de {@link BeanFactoryLocator} que sempre devolve uma mesma {@link BeanFactory}
     * 
     * @author takeshi
     */
    private static class FixedBeanFactoryLocator implements BeanFactoryLocator {

        private final BeanFactory beanFactory;

        /**
         * C'tor
         * 
         * @param beanFactory {@link BeanFactory}
         */
        public FixedBeanFactoryLocator(BeanFactory beanFactory) {
            super();
            this.beanFactory = beanFactory;
        }

        /**
         * {@inheritDoc}
         */
        public BeanFactoryReference useBeanFactory(String factoryKey) {
            return new BeanFactoryReference() {

                public void release() {
                }

                public BeanFactory getFactory() {
                    return beanFactory;
                }
            };
        }

    }

    /**
     * @return the messageListener
     */
    public MessageListener getMessageListener() {
        if (this.messageListener == null) {
            if (this.getBeanFactory().isSingleton(getMessageListenerBeanName())) {
                this.messageListener =
                        (MessageListener) this.getBeanFactory().getBean(this.getMessageListenerBeanName(), MessageListener.class);
            } else {
                return (MessageListener) this.getBeanFactory().getBean(this.getMessageListenerBeanName(), MessageListener.class);
            }
        }
        return messageListener;
    }

    /**
     * @param messageListener the messageListener to set
     */
    public void setMessageListener(MessageListener messageListener) {
        this.messageListener = messageListener;
    }

    /**
     * @return the messageListenerBeanName
     */
    public String getMessageListenerBeanName() {
        if (this.messageListenerBeanName == null) {
            this.messageListenerBeanName = lookupString(MESSAGE_LISTENER_BEAN_NAME_KEY);
        }
        ExceptionContext.getCurrentContext().put("messageListenerBeanName", this.messageListenerBeanName);
        return messageListenerBeanName;
    }

    /**
     * @param messageListenerBeanName the messageListenerBeanName to set
     */
    public void setMessageListenerBeanName(String messageListenerBeanName) {
        this.messageListenerBeanName = messageListenerBeanName;
    }

    /**
     * @return the transactionalController
     */
    public TransactionalController getTransactionalController() {
        if (this.transactionalController == null) {
            String beanName = this.getTransactionalControllerBeanName();
            if (beanName != null) {
                this.transactionalController =
                        (TransactionalController) this.getBeanFactory().getBean(beanName, TransactionalController.class);
            }
        }
        return transactionalController;
    }

    /**
     * @param transactionalController the transactionalController to set
     */
    public void setTransactionalController(TransactionalController transactionalController) {
        this.transactionalController = transactionalController;
    }

    /**
     * @return the transactionalControllerBeanName
     */
    public String getTransactionalControllerBeanName() {
        if (this.transactionalControllerBeanName == null && this.getBeanFactory() instanceof ListableBeanFactory) {
            ListableBeanFactory lbf = (ListableBeanFactory) getBeanFactory();
            String[] strings = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(lbf, TransactionalController.class);
            if (strings.length > 0) {
                this.transactionalControllerBeanName = strings[0];
            }
        }
        return transactionalControllerBeanName;
    }

    /**
     * @param transactionalControllerBeanName the transactionalControllerBeanName to set
     */
    public void setTransactionalControllerBeanName(String transactionalControllerBeanName) {
        this.transactionalControllerBeanName = transactionalControllerBeanName;
    }

    /**
     * Callback method to inject the annotation {@inheritDoc}
     */
    @PostConstruct
    @Override
    public synchronized void ejbCreate() {
        final ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
        final ClassLoader beanFactoryClassLoader = getClass().getClassLoader();
        if (currentClassLoader != beanFactoryClassLoader) {
            Thread.currentThread().setContextClassLoader(beanFactoryClassLoader);
        }
        try {
            super.ejbCreate();
        } finally {
            if (currentClassLoader != beanFactoryClassLoader) {
                Thread.currentThread().setContextClassLoader(currentClassLoader);
            }
        }
    }

    /**
     * Callback method to inject the annotation {@inheritDoc}
     */
    @PreDestroy
    @Remove
    @Override
    public synchronized void ejbRemove() {
        final ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
        final ClassLoader beanFactoryClassLoader = getClass().getClassLoader();
        if (currentClassLoader != beanFactoryClassLoader) {
            Thread.currentThread().setContextClassLoader(beanFactoryClassLoader);
        }
        try {
            super.ejbRemove();
            if (this.messageListener instanceof MessageDrivenBean) {
                ((MessageDrivenBean) this.messageListener).ejbRemove();
            }
        } finally {
            if (currentClassLoader != beanFactoryClassLoader) {
                Thread.currentThread().setContextClassLoader(currentClassLoader);
            }
        }
    }

}
